﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Input.Touch;
using Microsoft.Xna.Framework.Audio;

namespace Crosshatch
{
    /// <summary>
    /// Keep track of spawning and killing dots
    /// </summary>
    static class DotManager
    {
        /// <summary>
        /// List of dots currently on the screen
        /// </summary>
        private static List<Dot> dots;

        /// <summary>
        /// List of possible patterns
        /// </summary>
        private static List<DotPattern> patterns;

        /// <summary>
        /// List of lasers currently on the screen
        /// </summary>
        private static List<Laser> lasers;

        /// <summary>
        /// List of poptexts currently on the screen
        /// </summary>
        private static List<PopText> popTexts;

        /// <summary>
        /// List of dot graphics
        /// </summary>
        private static Dictionary<DotType, Texture2D> graphics;

        /// <summary>
        /// Counter that periodically ramps up
        /// </summary>
        public static int currentDifficulty;

        /// <summary>
        /// Amount of time before a diff change
        /// </summary>
        private static TimeSpan difficultyLength = new TimeSpan(0, 0, 10);

        /// <summary>
        /// Amount of time before next difficulty change
        /// </summary>
        private static TimeSpan nextDifficultyChange;

        /// <summary>
        /// The time between pattern spawns
        /// </summary>
        private static TimeSpan spawnDelay { get { return new TimeSpan(0, 0, 0, 0, (int)Math.Max(2000, 4000 - currentDifficulty * 500)); } }

        private static TimeSpan nextSpawn;

        /// <summary>
        /// The player's current score
        /// </summary>
        public static int score { get; private set; }

        private static int highscore;

        public static void ReportScore(int score)
        {
            if (score > highscore) highscore = score;
        }

        /// <summary>
        /// A spritefont
        /// </summary>
        public static SpriteFont ui;

        public static SpriteFont subui;

        private static SoundEffect laser;

        private static SoundEffect bing;

        /// <summary>
        /// The player's lives
        /// </summary>
        public static int health { get; private set; }

        /// <summary>
        /// The player's starting lives
        /// </summary>
        private const int startHealth = 10;

        /// <summary>
        /// Poptexts shown by number of simultaneous kills
        /// </summary>
        private static string[] praise;


        static DotManager()
        {
            dots = new List<Dot>();
            patterns = new List<DotPattern>();
            lasers = new List<Laser>();
            popTexts = new List<PopText>();

            DotPattern temp = new DotPattern(0);
            temp.AddDot(new DotPattern.DotSpawn(DotType.NORMAL, 0f, 0.3f));
            temp.AddDot(new DotPattern.DotSpawn(DotType.NORMAL, 1f, 0.5f));
            temp.AddDot(new DotPattern.DotSpawn(DotType.NORMAL, 2f, 0.7f));
            patterns.Add(temp);

            temp = new DotPattern(0);
            temp.AddDot(new DotPattern.DotSpawn(DotType.NORMAL, 0f, 0.3f));
            temp.AddDot(new DotPattern.DotSpawn(DotType.NORMAL, 0f, 0.5f));
            temp.AddDot(new DotPattern.DotSpawn(DotType.NORMAL, 0f, 0.7f));
            patterns.Add(temp);

            temp = new DotPattern(0);
            temp.AddDot(new DotPattern.DotSpawn(DotType.NORMAL, 0f, 0.3f));
            temp.AddDot(new DotPattern.DotSpawn(DotType.NORMAL, 0f, 0.4f));
            temp.AddDot(new DotPattern.DotSpawn(DotType.NORMAL, 0.8f, 0.6f));
            temp.AddDot(new DotPattern.DotSpawn(DotType.NORMAL, 0.8f, 0.7f));
            patterns.Add(temp);

            temp = new DotPattern(0);
            temp.AddDot(new DotPattern.DotSpawn(DotType.NORMAL, 0f, 0.5f));
            temp.AddDot(new DotPattern.DotSpawn(DotType.NORMAL, 0.6f, 0.3f));
            temp.AddDot(new DotPattern.DotSpawn(DotType.NORMAL, 0.6f, 0.5f));
            temp.AddDot(new DotPattern.DotSpawn(DotType.NORMAL, 0.6f, 0.7f));
            temp.AddDot(new DotPattern.DotSpawn(DotType.NORMAL, 1.2f, 0.5f));
            patterns.Add(temp);

            temp = new DotPattern(0);
            temp.AddDot(new DotPattern.DotSpawn(DotType.NORMAL, 0f, 0.3f));
            temp.AddDot(new DotPattern.DotSpawn(DotType.DAMAGE, 0f, 0.5f));
            temp.AddDot(new DotPattern.DotSpawn(DotType.NORMAL, 0f, 0.7f));
            patterns.Add(temp);

            temp = new DotPattern(0);
            temp.AddDot(new DotPattern.DotSpawn(DotType.DAMAGE, 0f, 0.5f));
            patterns.Add(temp);

            temp = new DotPattern(5);
            temp.AddDot(new DotPattern.DotSpawn(DotType.DAMAGE, 0f, 0.3f));
            temp.AddDot(new DotPattern.DotSpawn(DotType.DAMAGE, 0.5f, 0.5f));
            temp.AddDot(new DotPattern.DotSpawn(DotType.DAMAGE, 1f, 0.7f));
            patterns.Add(temp);

            praise = new string[9];
            praise[0] = "";
            praise[1] = "+1";
            praise[2] = "+3";
            praise[3] = "+6";
            praise[4] = "Great!";
            praise[5] = "Nice!";
            praise[6] = "Excellent!";
            praise[7] = "Amazing!";
            praise[8] = "Pro skills!";
        }


        /// <summary>
        /// Load all dot graphics
        /// </summary>
        /// <param name="content"></param>
        public static void LoadContent(ContentManager content)
        {
            graphics = new Dictionary<DotType, Texture2D>();
            graphics.Add(DotType.NORMAL, content.Load<Texture2D>("normal"));
            graphics.Add(DotType.DAMAGE, content.Load<Texture2D>("damage"));

            ui = content.Load<SpriteFont>("ui");
            subui = content.Load<SpriteFont>("subui");

            laser = content.Load<SoundEffect>("lasersnd");
            bing = content.Load<SoundEffect>("bing");

            Laser.LoadContent(content);
        }


        /// <summary>
        /// Get the graphic for the given dot type
        /// </summary>
        /// <param name="type"></param>
        /// <returns></returns>
        public static Texture2D GetGraphic(DotType type)
        {
            return graphics[type];
        }


        /// <summary>
        /// Create a new dot in the game
        /// </summary>
        /// <param name="dot"></param>
        public static void Spawn(DotPattern.DotSpawn dot, bool isVertical, bool isFlipped)
        {
            switch (dot.type)
            {
                case DotType.NORMAL:
                    dots.Add(new NormalDot(dot.GetSpawnPosition(isVertical, isFlipped)));
                    break;
                case DotType.DAMAGE:
                    dots.Add(new DamageDot(dot.GetSpawnPosition(isVertical, isFlipped)));
                    break;
            }
        }

        /// <summary>
        /// Reset all dots and patterns
        /// </summary>
        public static void Reset()
        {
            dots.Clear();
            lasers.Clear();
            popTexts.Clear();
            foreach (DotPattern p in patterns)
                p.Reset();
            currentDifficulty = 0;
            score = 0;
            health = startHealth;
            nextDifficultyChange = difficultyLength;
        }

        /// <summary>
        /// Take input to kill dots
        /// </summary>
        public static void TakeInput(TouchCollection input)
        {
            foreach (TouchLocation t in input)
            {
                if (t.State == TouchLocationState.Pressed)
                {
                    lasers.Add(new Laser(t.Position));
                    PlaySound(laser);
                }
            }
        }

        private static List<SoundEffectInstance> soundInstances = new List<SoundEffectInstance>();

        /// <summary>
        /// Play the specified sound
        /// </summary>
        /// <param name="sound"></param>
        public static void PlaySound(SoundEffect sound)
        {
            SoundEffectInstance inst = sound.CreateInstance();
            inst.Play();
            soundInstances.Add(inst);
        }

        /// <summary>
        /// Update spawning of dots
        /// </summary>
        /// <param name="gameTime"></param>
        public static void Update(GameTimerEventArgs gameTime)
        {
            //Change difficulty
            nextDifficultyChange -= gameTime.ElapsedTime;
            if (nextDifficultyChange <= TimeSpan.Zero)
            {
                nextDifficultyChange = difficultyLength;
                currentDifficulty++;
            }

            //Remove old sounds
            for (int c = soundInstances.Count - 1; c >= 0; c--)
                if (soundInstances[c].State == SoundState.Stopped)
                    soundInstances.RemoveAt(c);

            //Decide when to start patterns spawning
            nextSpawn -= gameTime.ElapsedTime;
            if (nextSpawn <= TimeSpan.Zero)
            {
                //Choose a pattern
                int usePattern = 0;
                do
                {
                    usePattern = Global.rand.Next(patterns.Count);
                } while (patterns[usePattern].Difficulty > currentDifficulty);
                patterns[usePattern].StartSpawn(gameTime);

                nextSpawn = spawnDelay;
            }

            //Spawn more dots
            foreach (DotPattern p in patterns)
                p.Update(gameTime);

            //Update poptexts
            for (int c = popTexts.Count - 1; c >= 0; c--)
            {
                popTexts[c].Update(gameTime);
                if (popTexts[c].Kill)
                    popTexts.RemoveAt(c);
            }

            Rectangle screen = new Rectangle(
                0, 0,
                Global.ScreenWidth,
                Global.ScreenHeight);
            for (int c = dots.Count - 1; c >= 0; c--)
            {
                //Update dots
                dots[c].Move(gameTime);

                //Remove offscreen dots
                if (!screen.Intersects(dots[c].GetRectangle()))
                {
                    if (!dots[c].damage) health--;
                    if (health < 0) health = 0;
                    dots.RemoveAt(c);
                }
            }

            //Calculate points for each kill
            int currentWorth = 1;
            int totalPoints = 0;
            for (int c = lasers.Count - 1; c >= 0; c--)
            {
                lasers[c].Update(gameTime);

                //Do laser collision
                if (lasers[c].Lethal)
                {
                    for (int d = dots.Count - 1; d >= 0; d--)
                    {
                        if (lasers[c].Collision(dots[d].GetRectangle()))
                        {
                            dots[d].Destroy();
                            if (dots[d].damage)
                            {
                                health--;
                                popTexts.Add(new PopText("Oops!", dots[d].position));
                                lasers[c].nullify = true;
                            }
                            else
                            {
                                totalPoints += currentWorth;
                                currentWorth++;
                                lasers[c].kills++;
                            }
                            dots.RemoveAt(d);
                        }
                    }
                }
                lasers[c].score += totalPoints;

                //Remove laser
                if (lasers[c].Dead)
                {
                    if (!lasers[c].nullify)
                    {
                        score += lasers[c].score;
                        popTexts.Add(new PopText(
                            praise[Math.Min(lasers[c].kills, praise.Length - 1)],
                            lasers[c].position));
                        if (lasers[c].kills > 0)
                            PlaySound(bing);
                    }
                    lasers.RemoveAt(c);
                }
            }
        }

        /// <summary>
        /// Draw all dots
        /// </summary>
        /// <param name="spriteBatch"></param>
        public static void Draw(SpriteBatch spriteBatch)
        {
            foreach (Dot d in dots)
                d.Draw(spriteBatch);

            foreach (Laser l in lasers)
                l.Draw(spriteBatch);

            foreach (PopText p in popTexts)
                p.Draw(spriteBatch);

            spriteBatch.DrawString(
                ui,
                "Score: " + score,
                new Vector2(10),
                Color.White);

            spriteBatch.DrawString(
                ui,
                "Best: " + highscore,
                new Vector2(10, 60),
                Color.White);

            spriteBatch.DrawString(
                ui,
                "x" + health,
                new Vector2(370, 10),
                Color.White);
        }


        /// <summary>
        /// Represents an element of text on the playing field
        /// </summary>
        private class PopText
        {
            private string text;
            private Vector2 position;
            private TimeSpan lifeTime;
            private static TimeSpan popTime = new TimeSpan(0, 0, 0, 0, 500);
            private static TimeSpan totalLifeTime = new TimeSpan(0, 0, 1);
            private static float popSpeed = 100;

            public PopText(string text, Vector2 position)
            {
                this.text = text;
                this.position = position;
                lifeTime = totalLifeTime;
            }

            /// <summary>
            /// Should this text be removed now?
            /// </summary>
            public bool Kill { get { return lifeTime <= TimeSpan.Zero; } }

            /// <summary>
            /// Update this poptext's lifetime
            /// </summary>
            /// <param name="e"></param>
            public void Update(GameTimerEventArgs e)
            {
                lifeTime -= e.ElapsedTime;
                if (totalLifeTime - lifeTime <= popTime)
                    position.Y -= popSpeed * (float)e.ElapsedTime.TotalSeconds;
            }

            public void Draw(SpriteBatch spriteBatch)
            {
                spriteBatch.DrawString(
                    DotManager.ui,
                    text,
                    position - DotManager.ui.MeasureString(text) / 2,
                    Color.White);
            }
        }


        /// <summary>
        /// Represents a laser hit on the screen
        /// </summary>
        private class Laser
        {
            /// <summary>
            /// The screen position of the center of the laser
            /// </summary>
            public Vector2 position { get; private set; }

            /// <summary>
            /// The time this laser has left to live
            /// </summary>
            private TimeSpan lifeTime;

            /// <summary>
            /// Laser overall lifetime
            /// </summary>
            private static TimeSpan totalLifeTime = new TimeSpan(0, 0, 0, 0, 300);

            /// <summary>
            /// Amount of time for which laser is lethal
            /// </summary>
            private static TimeSpan lethalTime = new TimeSpan(0, 0, 0, 0, 150);

            /// <summary>
            /// The width of the actual laser.
            /// </summary>
            private static int laserSize = 5;

            /// <summary>
            /// Does this laser's score count?
            /// </summary>
            public bool nullify = false;

            /// <summary>
            /// The laser graphic
            /// </summary>
            private static Texture2D graphic;

            /// <summary>
            /// Is this laser expired?
            /// </summary>
            public bool Dead { get { return lifeTime <= TimeSpan.Zero; } }

            /// <summary>
            /// Is this laser lethal?
            /// </summary>
            public bool Lethal { get { return totalLifeTime - lifeTime <= lethalTime; } }

            /// <summary>
            /// Points earned by this laser
            /// </summary>
            public int score = 0;

            /// <summary>
            /// Kills made by this laser
            /// </summary>
            public int kills = 0;


            /// <summary>
            /// Make a new laser object
            /// </summary>
            /// <param name="position"></param>
            public Laser(Vector2 position)
            {
                this.position = position;
                this.lifeTime = totalLifeTime;
            }

            /// <summary>
            /// Load the laser graphic
            /// </summary>
            /// <param name="content"></param>
            public static void LoadContent(ContentManager content)
            {
                graphic = content.Load<Texture2D>("laser");
            }

            /// <summary>
            /// Does the given rectangle collide with the laser?
            /// </summary>
            /// <param name="r"></param>
            public bool Collision(Rectangle r)
            {
                Rectangle collision = new Rectangle(
                    (int)position.X - graphic.Width,
                    (int)position.Y - laserSize / 2,
                    graphic.Width * 2,
                    laserSize);
                if (r.Intersects(collision)) return true;

                collision = new Rectangle(
                    (int)position.X - laserSize / 2,
                    (int)position.Y - graphic.Height,
                    laserSize,
                    graphic.Height * 2);
                if (r.Intersects(collision)) return true;

                return false;
            }

            /// <summary>
            /// Update the laser
            /// </summary>
            /// <param name="e"></param>
            public void Update(GameTimerEventArgs e)
            {
                lifeTime -= e.ElapsedTime;
            }

            /// <summary>
            /// Draw the laser
            /// </summary>
            /// <param name="batch"></param>
            public void Draw(SpriteBatch batch)
            {
                Color color = new Color(0, 1, 0,
                    (float)(lifeTime.TotalMilliseconds / totalLifeTime.TotalMilliseconds));
                batch.Draw(
                    graphic,
                    position,
                    null,
                    color,
                    0.0f,
                    new Vector2(0, graphic.Height),
                    1.0f,
                    SpriteEffects.None,
                    1.0f);
                batch.Draw(
                    graphic,
                    position,
                    null,
                    color,
                    0.0f,
                    new Vector2(graphic.Width, graphic.Height),
                    1.0f,
                    SpriteEffects.FlipHorizontally,
                    1.0f);
                batch.Draw(
                    graphic,
                    position,
                    null,
                    color,
                    (float)Math.PI,
                    new Vector2(0, graphic.Height),
                    1.0f,
                    SpriteEffects.None,
                    1.0f);
                batch.Draw(
                    graphic,
                    position,
                    null,
                    color,
                    (float)Math.PI,
                    new Vector2(graphic.Width, graphic.Height),
                    1.0f,
                    SpriteEffects.FlipHorizontally,
                    1.0f);
            }
        }
    }


    /// <summary>
    /// Defines the various types of dots
    /// </summary>
    public enum DotType { NORMAL, DAMAGE }


    /// <summary>
    /// A possible sequence of dots that might come up
    /// </summary>
    class DotPattern
    {
        /// <summary>
        /// A list of dots to spawn when this pattern is executed
        /// </summary>
        private List<DotSpawn> spawns;

        /// <summary>
        /// A list of dots that haven't been spawned yet in this execution
        /// </summary>
        private List<DotSpawn> spawnRemaining;

        /// <summary>
        /// The length of time this pattern has been spawning for
        /// </summary>
        private TimeSpan spawnDuration;

        /// <summary>
        /// Is this pattern currently spawning?
        /// </summary>
        public bool isSpawning { get; private set; }

        /// <summary>
        /// Controls whether the dots spawn on the pos or neg screen side
        /// </summary>
        private bool isFlipped;

        /// <summary>
        /// Controls whether the dots move vertically or horizontally
        /// </summary>
        private bool isVertical;

        /// <summary>
        /// The difficulty of this pattern (arbitrary level number at which it can spawn)
        /// </summary>
        private int difficulty;

        public int Difficulty { get { return difficulty; } }


        public DotPattern(int difficulty)
        {
            this.difficulty = difficulty;
            spawns = new List<DotSpawn>();
            Reset();
        }

        /// <summary>
        /// Continue spawning this pattern if it needs to.
        /// </summary>
        /// <param name="gameTime"></param>
        public void Update(GameTimerEventArgs gameTime)
        {
            if (isSpawning)
            {
                spawnDuration += gameTime.ElapsedTime;

                for (int c = spawnRemaining.Count - 1; c >= 0; c--)
                {
                    if (spawnDuration.TotalSeconds >= spawnRemaining[c].spawnTime)
                    {
                        DotManager.Spawn(spawnRemaining[c], isVertical, isFlipped);
                        spawnRemaining.RemoveAt(c);
                    }
                }

                //End this pattern
                if (spawnRemaining.Count == 0) Reset();
            }
        }

        public void Reset()
        {
            isSpawning = false;
            spawnDuration = TimeSpan.Zero;
            spawnRemaining = null;
        }

        /// <summary>
        /// Start executing this spawn pattern
        /// </summary>
        /// <param name="gameTime"></param>
        public void StartSpawn(GameTimerEventArgs gameTime)
        {
            isSpawning = true;
            spawnDuration = TimeSpan.Zero;
            spawnRemaining = new List<DotSpawn>();
            foreach (DotSpawn d in spawns) spawnRemaining.Add(d);
            isVertical = Global.rand.Next(2) == 0;
            isFlipped = Global.rand.Next(2) == 0;
        }

        /// <summary>
        /// Add a dot to this spawn pattern
        /// </summary>
        /// <param name="dot"></param>
        public void AddDot(DotSpawn dot)
        {
            spawns.Add(dot);
        }

        /// <summary>
        /// Represents the spawn of one dot
        /// </summary>
        public struct DotSpawn
        {
            /// <summary>
            /// The dot to spawn
            /// </summary>
            public DotType type;

            /// <summary>
            /// When to spawn the dot, in seconds relative to pattern start
            /// </summary>
            public float spawnTime;

            /// <summary>
            /// The position to spawn the dot, in ratios of screen edge
            /// </summary>
            public float position;


            public DotSpawn(DotType dot, float spawnTime, float position)
            {
                this.type = dot;
                this.spawnTime = spawnTime;
                this.position = position;
            }

            /// <summary>
            /// Get position to spawn the dot
            /// </summary>
            public Vector2 GetSpawnPosition(bool isVertical, bool isFlipped)
            {
                Vector2 oposition = Vector2.Zero;
                if (isVertical)
                {
                    oposition.X = Global.ScreenWidth * position;
                    if (isFlipped)
                        oposition.Y = Global.ScreenHeight + DotManager.GetGraphic(type).Height / 2;
                    else
                        oposition.Y = 0 - DotManager.GetGraphic(type).Height / 2;
                }
                else
                {
                    oposition.Y = Global.ScreenHeight * position;
                    if (isFlipped)
                        oposition.X = Global.ScreenWidth + DotManager.GetGraphic(type).Width / 4;
                    else
                        oposition.X = 0 - DotManager.GetGraphic(type).Width / 4;
                }
                return oposition;
            }
        }
    }
}
